/*
 * Copyrignt (c) xuzeshui.com. All Rights Reserved.
 * Author: Zeshui Xu<i@xuzeshui.com>
 * Created Time: 2015-11-09 18:53:02
 * Last Modified: 2017-07-21 21:25:51
 * File Name: captcha/captcha.go
 * Description: 验证码处理
 */

package captcha

import (
	"fmt"
	"math/rand"
	"time"

	"doubimeizhi.com/utility"
	"passport/account"
	"passport/errorcode"
	"passport/models"
	smsrpcclent "passport/rpc/client/sms"

	log "github.com/cihub/seelog"
	"github.com/garyburd/redigo/redis"
)

//验证码类型
const (
	CAPTCHA_MOBILE_REGISTER           uint = 1 //注册验证码
	CAPTCHA_MOBILE_LOGIN              uint = 2 //登陆验证码
	CAPTCHA_MOBILE_RESET_PASSWORD     uint = 3 //找回密码验证码
	CAPTCHA_MOBILE_CHANGE_PASSWORD    uint = 4 //修改密码验证码
	CAPTCHA_MOBILE_BIND               uint = 5 //手机绑定验证码
	CAPTCHA_MOBILE_UNBIND             uint = 6 //手机解绑验证码
	CAPTCHA_MOBILE_BIND_THIRD_ACCOUNT uint = 7 //第三方账号登录绑定手机号
)

//不同应用不同验证码类型的短信模板编号
var (
	smsTplCaptchaDefault   map[uint]int
	smsTplCaptchaQingke    map[uint]int
	smsTplCaptchaFingertip map[uint]int
)

var (
	captchaService *CaptchaService
)

/*
	有效验证码检查
*/
func IsValidCaptchaType(bid uint) bool {
	return bid >= 1 && bid <= 7
}

/*
	发送验证码请求参数
*/
type SendCapchaReq struct {
	AppKey string //应用标识
	Mobile string `json:"mobile"`
	Bid    uint   `json:"bid"`
}

/*
	验证码验证
*/
type ValidateCaptchaReq struct {
	Mobile  string `json:"mobile"`
	Bid     uint   `json:"bid"`
	Captcha string `json:"captcha"`
}

/*
	验证码信息存储格式
*/
type CaptchaInfo struct {
	Captcha  string `redis:"captcha"`
	SendTick int64  `redis:"send_tick"`
	Times    int32  `redis:"times"` //一定时间内发送的次数,做限制使用
}

/*
	CaptchaService
*/
type CaptchaService struct{}

/*
	存储key的格式
*/
func (p *CaptchaService) GetCaptchaInfoKey(mobile string, bid uint) string {
	return fmt.Sprintf("c:%d:%s", bid, mobile)
}

/*
	获取CaptchaInfo信息
	当error不为nil表示有错误
	当error为nil且第一参数为nil,表示不存在
*/
func (p *CaptchaService) getCaptchaInfoItem(mobile string, bid uint) (*CaptchaInfo, int64, error) {
	conn := models.GetPassportModel().Get()
	defer conn.Close()
	var info CaptchaInfo
	var ttl int64
	rkey := p.GetCaptchaInfoKey(mobile, bid)
	conn.Send("hgetall", rkey)
	conn.Send("ttl", rkey)
	conn.Flush()
	if reply, err := redis.Values(conn.Receive()); err == nil {
		if len(reply) == 0 {
			return nil, 0, nil
		}
		if err := redis.ScanStruct(reply, &info); err == nil {
			ttl, _ = redis.Int64(conn.Receive())
			return &info, ttl, nil
		} else {
			return nil, 0, err
		}
	} else if redis.ErrNil == err {
		return nil, 0, nil
	} else {
		return nil, 0, err
	}
}

func (p *CaptchaService) GetCaptchaInfoItem(mobile string, bid uint) (*CaptchaInfo, int64, error) {
	return p.getCaptchaInfoItem(mobile, bid)
}

/*
	保存CaptchaInfo信息
*/
func (p *CaptchaService) saveCaptchaInfoItem(mobile string, bid uint, captchaItem *CaptchaInfo, ttl int64) error {
	rkey := p.GetCaptchaInfoKey(mobile, bid)
	conn := models.GetPassportModel().Get()
	defer conn.Close()
	conn.Send("hmset", redis.Args{}.Add(rkey).AddFlat(captchaItem)...)
	_, err := conn.Do("expire", rkey, ttl)
	return err
}

/*
	删除验证码,让其立即失效
*/
func (p *CaptchaService) DeleteCaptchaInfoItem(mobile string, bid uint) error {
	rkey := p.GetCaptchaInfoKey(mobile, bid)
	conn := models.GetPassportModel().Get()
	defer conn.Close()
	_, err := conn.Do("del", rkey)
	return err
}

/*
	请求向手机发送验证码
*/
func (p *CaptchaService) SendCaptchaForMobile(req *SendCapchaReq) (uint, string) {
	if nil == req || !utility.IsValidMainlandMobile(req.Mobile) {
		return errorcode.EC_CAPTCHA_INVALID_MOBILE_NUMBER, "invalid mobile number"
	}

	switch req.Bid {
	case CAPTCHA_MOBILE_REGISTER: //手机注册
		return p.sendCaptchaForMobileRegister(req.Mobile, req.AppKey)
	case CAPTCHA_MOBILE_LOGIN: //验证码登录
		return p.sendCaptchaItem(req.Mobile, CAPTCHA_MOBILE_LOGIN, req.AppKey)
	case CAPTCHA_MOBILE_RESET_PASSWORD:
		return p.sendCaptchaForMobileResetPassword(req.Mobile, req.AppKey)
	case CAPTCHA_MOBILE_BIND:
		return p.sendCaptchaForMobileBind(req.Mobile, req.AppKey)
	case CAPTCHA_MOBILE_BIND_THIRD_ACCOUNT:
		return p.sendCaptchaForMobileBindThirdAccount(req.Mobile, req.AppKey)
	default:
		return errorcode.EC_CAPTCHA_NOT_SUPPORT_BID, "NOT support bid"
	}

}

/*
	手机验证码验证
*/
func (p *CaptchaService) ValidateCaptchaForMobile(req *ValidateCaptchaReq) (uint, string) {
	if nil == req || !utility.IsValidMainlandMobile(req.Mobile) {
		return errorcode.EC_CAPTCHA_INVALID_MOBILE_NUMBER, "invalid mobile number"
	}

	if req.Captcha == "" {
		return errorcode.EC_CAPTCHA_NOT_CORRECT, "not correct"
	}

	if !IsValidCaptchaType(req.Bid) {
		return errorcode.EC_CAPTCHA_NOT_SUPPORT_BID, "NOT support bid"
	}

	captchaItem, _, err := p.getCaptchaInfoItem(req.Mobile, req.Bid)
	if err != nil {
		log.Warnf("getCaptchaInfoItem error, mobile:%s, bid: %d, msg: %s", req.Mobile, req.Bid, err.Error())
		return errorcode.EC_GL_INTERNAL_ERROR, errorcode.Msg(errorcode.EC_GL_INTERNAL_ERROR)
	}

	if captchaItem == nil {
		return errorcode.EC_CAPTCHA_NOT_CORRECT, "not correct"
	}

	if captchaItem.Captcha == req.Captcha {
		return errorcode.EC_GL_SUCCESS, errorcode.Msg(errorcode.EC_GL_SUCCESS)
	}
	return errorcode.EC_CAPTCHA_NOT_CORRECT, "not correct"
}

/*
	手机注册验证码
*/
func (p *CaptchaService) sendCaptchaForMobileRegister(mobile string, appkey string) (uint, string) {
	if ai, err := account.GetAccountService().GetAllAccountInfo(account.ACCOUNT_TYPE_MOBILE, mobile); err != nil { //查询账号异常
		log.Warnf("查询账号异常 error: %s", err.Error())
		return errorcode.EC_GL_INTERNAL_ERROR, errorcode.Msg(errorcode.EC_GL_INTERNAL_ERROR)
	} else if ai != nil { //账号存在
		log.Warnf("sendCaptchaForMobileRegister register yet, mobile: %s", mobile)
		return errorcode.EC_REGISTER_MOBILE_REGISTED, "register yet"
	}
	return p.sendCaptchaItem(mobile, CAPTCHA_MOBILE_REGISTER, appkey)
}

/*
	手机重置密码验证码
*/
func (p *CaptchaService) sendCaptchaForMobileResetPassword(mobile string, appkey string) (uint, string) {
	if ai, err := account.GetAccountService().GetAllAccountInfo(account.ACCOUNT_TYPE_MOBILE, mobile); err != nil {
		return errorcode.EC_GL_INTERNAL_ERROR, errorcode.Msg(errorcode.EC_GL_INTERNAL_ERROR)
	} else if ai == nil {
		return errorcode.EC_RESETPASS_ACCOUNT_NOT_EXISTS, "mobile is not exists"
	} else {
		if ai.IsFreezed() {
			log.Warnf("MobileResetPassword mobile is freezed. mobile: %s", mobile)
			return errorcode.EC_RESETPASS_ACCOUNT_FREEZE, "mobile is freezed"
		}

		if !ai.IsBinded() {
			return errorcode.EC_RESETPASS_ACCOUNT_NOT_EXISTS, "mobile is not exists"
		}
	}
	return p.sendCaptchaItem(mobile, CAPTCHA_MOBILE_RESET_PASSWORD, appkey)
}

/*
	手机绑定验证码
*/
func (p *CaptchaService) sendCaptchaForMobileBind(mobile string, appkey string) (uint, string) {
	if ai, err := account.GetAccountService().GetAllAccountInfo(account.ACCOUNT_TYPE_MOBILE, mobile); err != nil {
		return errorcode.EC_GL_INTERNAL_ERROR, errorcode.Msg(errorcode.EC_GL_INTERNAL_ERROR)
	} else if ai != nil && ai.BindStatus == account.AI_BIND_STATUS { //账号已绑定
		log.Warnf("sendCaptchaForMobileBind mobile binded yest. mobile: %s", mobile)
		return errorcode.EC_ACCOUNT_BIND_BINDED, "account binded yet"
	}
	return p.sendCaptchaItem(mobile, CAPTCHA_MOBILE_BIND, appkey)
}

/*
	第三方账号登录绑定手机号
*/
func (p *CaptchaService) sendCaptchaForMobileBindThirdAccount(mobile string, appkey string) (uint, string) {
	return p.sendCaptchaItem(mobile, CAPTCHA_MOBILE_BIND_THIRD_ACCOUNT, appkey)
}

func (p *CaptchaService) sendCaptchaItem(mobile string, bid uint, appkey string) (uint, string) {
	captchaItem, ttl, err := p.getCaptchaInfoItem(mobile, bid)
	if err != nil {
		log.Errorf("getCaptchaInfoItem error, mobile:%s, bid: %d, msg: %s", mobile, bid, err.Error())
		return errorcode.EC_GL_INTERNAL_ERROR, errorcode.Msg(errorcode.EC_GL_INTERNAL_ERROR)
	}

	if captchaItem != nil {
		currentTick := time.Now().Unix()
		if captchaItem.SendTick+models.GetCaptchaSendIntervalSecond() > currentTick { //发送太频繁
			log.Warnf("sendCaptchaItem too frequence, mobile: %s, bid: %d", mobile, bid)
			return errorcode.EC_CAPTCHA_TOO_FREQUENCE, "send too frequence, wait for a moment"
		}

		if int(captchaItem.Times) >= models.GetCaptchaSendLimitTimes() { //达到发送的上限次数
			log.Warnf("sendCaptchaItem reach the limit times: %d, mobile: %s, bid: %d", models.GetCaptchaSendLimitTimes(), mobile, bid)
			return errorcode.EC_CAPTCHA_REACH_LIMIT_TIMES, "reach the limit times"
		}
		captchaItem.SendTick = time.Now().Unix()
		captchaItem.Times += 1
		p.saveCaptchaInfoItem(mobile, bid, captchaItem, ttl)

	} else {
		captchaItem = &CaptchaInfo{
			Captcha:  p.generateCaptchaCode(mobile),
			SendTick: time.Now().Unix(),
			Times:    1,
		}
		p.saveCaptchaInfoItem(mobile, bid, captchaItem, models.GetCaptchaTTL())
	}

	p.sendCaptchaCode(mobile, captchaItem.Captcha, bid, appkey)

	return errorcode.EC_GL_SUCCESS, errorcode.Msg(errorcode.EC_GL_SUCCESS)
}

/*
	判断是否测试手机号,生成固定的验证码,同时不需要发送短信
*/
func (p *CaptchaService) isMobileForTestFixed(mobile string) bool {
	return "13000000000" == mobile || "15000000000" == mobile ||
		"17000000000" == mobile || "18000000000" == mobile
}

/*
	生成验证码
*/
func (p *CaptchaService) generateCaptchaCode(mobile string) string {
	if p.isMobileForTestFixed(mobile) {
		return "161130"
	}
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	val := 100000 + r.Intn(899999)
	//val = 1234 //强制设置为1234,测试使用,等短信平台好了之后再使用随机值
	return fmt.Sprintf("%d", val)
}

/*
	发送验证码短信
*/
func (p *CaptchaService) sendCaptchaCode(mobile, captcha string, bid uint, appkey string) {
	ok, tplId := p.smsTpl(bid, appkey)
	if ok && !p.isMobileForTestFixed(mobile) {
		go smsrpcclent.SendSMSTpl(mobile, tplId, map[string]string{"validatecode": captcha})
	}
	log.Infof("sendCaptchaCode mobile: %s, captcha: %s, bid: %d, tplId: %d, appkey: %s", mobile, captcha, bid, tplId, appkey)
}

/*
	根据不同的appkey返回不同的tplid
*/
func (p *CaptchaService) smsTpl(bid uint, appkey string) (bool, int) {
	switch appkey {
	case "271d8700b1074a9582a8cc4cdff94c5f":
		fallthrough
	case "0272050d21d641e2b6f188fde6e48476":
		fallthrough
	case "e3b01549970b4a65a3b29f8e2c12939b":
		if tplId, ok := smsTplCaptchaQingke[bid]; ok {
			return true, tplId
		} else {
			return p.smsTplDefault(bid)
		}
	case "ff983cecc76d406a8b1c7c3c1b7cd172":
		fallthrough
	case "cc0dec8c07444e9dadb75d41218c4fe4":
		if tplId, ok := smsTplCaptchaFingertip[bid]; ok {
			return true, tplId
		} else {
			return p.smsTplDefault(bid)
		}
	default:
		return p.smsTplDefault(bid)
	}
}

//根据bid根据默认的短信模板
func (p *CaptchaService) smsTplDefault(bid uint) (bool, int) {
	if tplId, ok := smsTplCaptchaDefault[bid]; ok {
		return true, tplId
	}
	return false, 0
}

func GetCaptchaService() *CaptchaService {
	return captchaService
}

func init() {
	captchaService = &CaptchaService{}
	smsTplCaptchaDefault = map[uint]int{
		CAPTCHA_MOBILE_REGISTER:           1,
		CAPTCHA_MOBILE_LOGIN:              2,
		CAPTCHA_MOBILE_RESET_PASSWORD:     3,
		CAPTCHA_MOBILE_BIND:               4,
		CAPTCHA_MOBILE_BIND_THIRD_ACCOUNT: 4,
	}
	smsTplCaptchaQingke = map[uint]int{
		CAPTCHA_MOBILE_REGISTER:           8,
		CAPTCHA_MOBILE_LOGIN:              9,
		CAPTCHA_MOBILE_RESET_PASSWORD:     10,
		CAPTCHA_MOBILE_BIND:               11,
		CAPTCHA_MOBILE_BIND_THIRD_ACCOUNT: 11,
	}
	smsTplCaptchaFingertip = map[uint]int{
		CAPTCHA_MOBILE_REGISTER:           12,
		CAPTCHA_MOBILE_LOGIN:              13,
		CAPTCHA_MOBILE_RESET_PASSWORD:     14,
		CAPTCHA_MOBILE_BIND:               15,
		CAPTCHA_MOBILE_BIND_THIRD_ACCOUNT: 15,
	}
}
